﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using NUnit.Framework;
using Spring.Objects.Factory.Config;
using Spring.Objects.Factory.Support;
using Strongshell.Recoil.Core.Composition;
using Strongshell.Recoil.Tests.Utility;

namespace Strongshell.Recoil.Tests.Composition
{
    [TestFixture]
    public class Examples : WiringContainer
    {
        [SetUp]
        public void Setup()
        {
            ObjectConfigurations.Clear();
        }

        [Test]
        public void PropertyInjectListWithEmbeddedMemberInit()
        {
            const string testString = "Test";

            Define<MockType>()
                .Set(m => m.ListProperty).To((() => new List<MockType>
                                                        {
                                                            new MockType { StringProperty = testString }
                                                        }));

            // There must be two object configurations, one for the explicit definition and the other for the embedded MockType
            Assert.AreEqual(ObjectConfigurations.Count, 2);

            var firstDefinition = ObjectConfigurations[0].Builder.ObjectDefinition;

            var injectedList = firstDefinition.GetPropertyByName<ManagedList>("ListProperty");

            // Ensure that the injected list is not empty
            Assert.IsNotEmpty(injectedList);

            // The first and only object in the list must be a RuntimeObjectReference
            Assert.That(injectedList[0] is RuntimeObjectReference);

            var reference = (RuntimeObjectReference) injectedList[0];

            // The reference name must match the object name
            Assert.AreEqual(ObjectConfigurations[1].Name, reference.ObjectName);

            var secondDefinition = ObjectConfigurations[1].Builder.ObjectDefinition;

            var injectedString = secondDefinition.GetPropertyByName<String>("StringProperty");

            // The injected property value must be correct
            Assert.AreEqual(injectedString, testString);
        }

        [Test]
        public void ConstructorInjectInstanceFactoryInstantiatedObjectWithArgument()
        {
            const string testArg = "arg";

            Define(() => new MockType(Wire<IMockInterfaceFactory>().Create(testArg)));

            var firstDefinition = ObjectConfigurations[0].Builder.ObjectDefinition;

            var dependencyReference = firstDefinition.GetConstructorArgByIndex<RuntimeObjectReference>(0);

            // The reference name must match the referred object name
            Assert.AreEqual(dependencyReference.ObjectName, ObjectConfigurations[1].Name);

            var secondDefinition = ObjectConfigurations[1].Builder.ObjectDefinition;

            // The factory method name must be correct
            Assert.AreEqual(secondDefinition.FactoryMethodName, "Create");

            // The factory object name must be generated from the supplied interface
            Assert.AreEqual(secondDefinition.FactoryObjectName, NameFactory.CreateName<IMockInterfaceFactory>());
        }

        [Test]
        public void PropertyInjectNestedStaticAndInstanceFactoryInstantiatedObject()
        {
            const string customName = "myCustomName";

            Define<MockType>().Set(m => m.InterfaceProperty).To(
                () => MockInterfaceFactory.Create(Wire<IMockInterface>(customName)));

            var firstDefinition = ObjectConfigurations[0].Builder.ObjectDefinition;

            var propertyDependency = firstDefinition.GetPropertyByName<RuntimeObjectReference>("InterfaceProperty");

            // The reference name must match the referred object name
            Assert.AreEqual(propertyDependency.ObjectName, ObjectConfigurations[1].Name);

            var secondDefinition = ObjectConfigurations[1].Builder.ObjectDefinition;

            var constructorDependency = secondDefinition.GetConstructorArgByIndex<RuntimeObjectReference>(0);

            // The reference name must match the custom name provided in the wiring pattern
            Assert.AreEqual(constructorDependency.ObjectName, customName);

            // The factory method name must be correct
            Assert.AreEqual(secondDefinition.FactoryMethodName, "Create");

            // The object type must be set to the factory class type
            Assert.AreEqual(secondDefinition.ObjectType, typeof(MockInterfaceFactory));
        }

        [Test]
        public void ConstructorInjectNestedArrayAndDictionary()
        {
            const string entry1 = "Entry1";
            const string entry2 = "Entry2";
            const string entry3 = "Entry3";
            const string entry4 = "Entry4";

            Define<MockListContainerDictionary>().Set(c => c.Dictionary)
                .To(() => new Dictionary<string, IMockInterface[]>
                              {
                                  {entry1, new []{Wire<IMockInterface>()}} // Add an array containing wire injected dependency
                                  ,{entry2, new []{Wire<IMockInterfaceFactory>().Create("Arg")}} // Instance factory
                                  ,{entry3, new []{MockInterfaceFactory.Create(42)}} // Static factory
                                  ,{entry4, new []{DirectMethodCallInstantiation()}} // Use object directly from call result
                              });

            var firstDefinition = ObjectConfigurations[0].Builder.ObjectDefinition;

            var managedDictionary = firstDefinition.GetPropertyByName<ManagedDictionary>("Dictionary");

            var firstReference = GetReference(entry1, managedDictionary);

            Assert.AreEqual(firstReference.ObjectName, NameFactory.CreateName<IMockInterface>());

            var secondReference = GetReference(entry2, managedDictionary);

            Assert.AreEqual(ObjectConfigurations[1].Name, secondReference.ObjectName);

            var thirdReference = GetReference(entry3, managedDictionary);

            Assert.AreEqual(ObjectConfigurations[2].Name, thirdReference.ObjectName);

            var fourthValue = ((ManagedList)managedDictionary[entry4])[0] as MockInterfaceImplementation;

            Assert.IsNotNull(fourthValue);

            var secondDefinition = ObjectConfigurations[1].Builder.ObjectDefinition;

            Assert.AreEqual(secondDefinition.FactoryObjectName, NameFactory.CreateName<IMockInterfaceFactory>());
            Assert.AreEqual(secondDefinition.FactoryMethodName, "Create");

            var instanceFactoryMethodArg = secondDefinition.GetConstructorArgByIndex<string>(0);

            Assert.AreEqual(instanceFactoryMethodArg, "Arg");

            var thirdDefinition = ObjectConfigurations[2].Builder.ObjectDefinition;

            Assert.AreEqual(thirdDefinition.ObjectType, typeof(MockInterfaceFactory));
            Assert.AreEqual(thirdDefinition.FactoryMethodName, "Create");

            var staticFactoryMethodArg = thirdDefinition.GetConstructorArgByIndex<int>(0);

            Assert.AreEqual(staticFactoryMethodArg, 42);
        }

        private static IMockInterface DirectMethodCallInstantiation()
        {
            return new MockInterfaceImplementation();
        }

        private static RuntimeObjectReference GetReference(string key, IDictionary managedDictionary)
        {
            var list = (ManagedList) managedDictionary[key];

            return (RuntimeObjectReference) list[0];
        }

        [Test]
        public void ConstructorInjectNestedMemberInitWithDelegateProperty()
        {
            Action<string> action = s => s.ToString();

            Define<MockDelegateContainer>().Set(c => c.MockAction).To(() => action);

            var definition = ObjectConfigurations[0].Builder.ObjectDefinition;

            var propertyValue = definition.GetPropertyByName<Action<string>>("MockAction");

            // The delegate must be injected directly
            Assert.AreEqual(propertyValue, action);
        }

        [Test]
        public void ConstructorInjectNestedMemberInitWithExpressionProperty()
        {
            Expression<Func<String>> expression = () => "Return";

            Define<MockExpressionContainer>().Set(c => c.MockExpression).To(() => expression);

            var definition = ObjectConfigurations[0].Builder.ObjectDefinition;

            var propertyValue = definition.GetPropertyByName<Expression<Func<String>>>("MockExpression");

            // The expression must be injected directly
            Assert.AreEqual(propertyValue, expression);
        }

        [Test]
        public void SetupDictionaryWithCompositeKeys()
        {
            Define(() => new MockListContainerDictionary
                             {
                                 CompositeKeyDictionary = new Dictionary<CompositeKey, IMockInterface>
                                                              {
                                                                  { new CompositeKey("1", "1"), new MockInterfaceImplementation(Wire<IMockInterface>()) },
                                                                  { new CompositeKey("2", "2"), new MockInterfaceImplementation(Wire<IMockInterface>()) }
                                                              }
                             });

            Assert.AreEqual(ObjectConfigurations.Count, 5);

            var firstKeyDefinition = ObjectConfigurations[1].Builder.ObjectDefinition;

            Assert.AreEqual(firstKeyDefinition.ConstructorArgumentValues.IndexedArgumentValues.Count, 2);
            Assert.AreEqual(firstKeyDefinition.GetConstructorArgByIndex<string>(0), "1");
            Assert.AreEqual(firstKeyDefinition.GetConstructorArgByIndex<string>(1), "1");

            var secondKeyDefinition = ObjectConfigurations[3].Builder.ObjectDefinition;

            Assert.AreEqual(secondKeyDefinition.ConstructorArgumentValues.IndexedArgumentValues.Count, 2);
            Assert.AreEqual(secondKeyDefinition.GetConstructorArgByIndex<string>(0), "2");
            Assert.AreEqual(secondKeyDefinition.GetConstructorArgByIndex<string>(1), "2");

            var firstSubObject = ObjectConfigurations[2].Builder.ObjectDefinition;

            var firstReference = firstSubObject.GetConstructorArgByIndex<RuntimeObjectReference>(0);

            Assert.AreEqual(firstReference.ObjectName, NameFactory.CreateName<IMockInterface>());

            var secondSubObject = ObjectConfigurations[4].Builder.ObjectDefinition;

            var secondReference = secondSubObject.GetConstructorArgByIndex<RuntimeObjectReference>(0);

            Assert.AreEqual(secondReference.ObjectName, NameFactory.CreateName<IMockInterface>());
        }

        [Test]
        public void ParentObjectPropertyAccessFromChildObject()
        {
            const string testString = "Test";

            var parent = Define<MockType>()
                .Set(m => m.StringProperty).To(() => testString);
            var child = Define<MockChildType>(parent);

            var objectFactory = new DefaultListableObjectFactory();
            objectFactory.RegisterObjectDefinition(parent.Name, ObjectConfigurations[0].Builder.ObjectDefinition);
            objectFactory.RegisterObjectDefinition(child.Name, ObjectConfigurations[1].Builder.ObjectDefinition);

            var childResolved = objectFactory[child.Name] as MockChildType;

            // Parent property should be available in child object
            Assert.AreEqual(childResolved.StringProperty, testString);
        }

        [Test]
        public void ParentObjectFactoryInstantiatedWithPropertyAccessFromChildObject()
        {
            const string testString = "Test";

            // argument of the ctor will be available in StringProperty after instantiation
            var parent = Define(() => new MockType(testString));
            var child = Define(() => new MockChildType(), parent);

            var objectFactory = new DefaultListableObjectFactory();
            objectFactory.RegisterObjectDefinition(parent.Name, ObjectConfigurations[0].Builder.ObjectDefinition);
            objectFactory.RegisterObjectDefinition(child.Name, ObjectConfigurations[1].Builder.ObjectDefinition);

            Assert.That(objectFactory[child.Name] is MockChildType);

            var childResolved = objectFactory[child.Name] as MockChildType;

            // Parent property (that was set in parent constructor) should be available in child object
            Assert.AreEqual(childResolved.StringProperty, testString);
        }

        #region Omitted Implementation

        public override void SetupContainer()
        {

        }

        #endregion
    }
}
